
#define GNU_SOURCE_  // For getline()

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include <vector>

/*#define DEBUG_PARSE*/
/*#define DEBUG_UPLOAD*/
/*#define DEBUG_DOWNLOAD*/

#define MAX_UPLOAD_BYTES 100000

char *g_data;
int g_databytes;
#define MAX_APP_NAME 32
char g_app[MAX_APP_NAME];
#define DIR_NAME_CHARS 12
#define DIR_NAME_TEMPLATE "/tmp/XXXXXX"
char g_dirname[DIR_NAME_CHARS];
#define FILE_NAME_CHARS DIR_NAME_CHARS+16
char g_inputfilename[FILE_NAME_CHARS];
char g_intermediatefilename[FILE_NAME_CHARS];
char g_resultfilename[FILE_NAME_CHARS];
char g_commentfilename[FILE_NAME_CHARS];
char g_zipfilename[FILE_NAME_CHARS];

char *g_remote_addr;
char *g_remote_host;
char *g_remote_user;

#define WHITE_LIST "whitelist.txt"
size_t g_white_list_size;
char *g_white_list;

static int write_stdout(const char *file);

typedef std::vector<std::string> string_vector_t;

class WhiteList {
 public:
  static WhiteList* CreateWhiteList(const char* white_list_file) {
    size_t len = 0;
    ssize_t read;
    FILE* fp = fopen(white_list_file, "r");
    if (fp == NULL) {
      return NULL;
    }
    WhiteList* white_list = new WhiteList;
    char* line;
    while ((read = getline(&line, &len, fp)) != -1) {
      char* nl = index(line, '\n');
      if (nl) {
        *nl = 0;
      }
      white_list->AddEntry(line);
    }
    return white_list;
  }

  bool Allow(const std::string& ipaddr) const {
    for (int i = 0; i < white_list_.size(); ++i) {
      if (ipaddr.compare(0, white_list_[i].length(), white_list_[i]) == 0) {
        return true;
      }
    }
    return false;
  }

  void Debug(const std::string& ipaddr) const {
    for (int i = 0; i < white_list_.size(); ++i) {
      std::cout << i << " " << white_list_[i] << std::endl;
      std::cout << "compare " << ipaddr.compare(0, white_list_[i].length(), white_list_[i]) << std::endl;
    }
  }

  void Print() const {
    for (int i = 0; i < white_list_.size(); ++i) {
      std::cout << white_list_[i] << std::endl;
    }
  }

 private:
  WhiteList() {}
  void AddEntry(const char* entry) {
    white_list_.push_back(entry);
  }
  string_vector_t white_list_;
};


void
print_toobig(int nbytes)
{
#ifdef DEBUG_PARSE
  printf("input too big %d, max %d\n\n", nbytes, MAX_UPLOAD_BYTES);
  fflush(stdout);
#else
  printf("Content-type: text/html\n\n");
  fflush(stdout);
  write_stdout("toobig.html");
  //printf("<html><title>TOO BIG</title><body><p>TOO TOO BIG</p></body></html>");
#endif
}

void
print_access_denied()
{
  printf("Content-type: text/html\n\n");
  fflush(stdout);
  write_stdout("denied.html");
}

char *
read_stdin(int nbytes)
{
  int count, c;
  char *cp;
  if (nbytes <= 0) {
    return NULL;
  }

  if (nbytes > MAX_UPLOAD_BYTES) {
    return NULL;
  }
  cp = (char*)malloc(nbytes + 1);
  if (cp == NULL) {
    return NULL;
  }

  for(count = 0; c = read(0, cp + count, nbytes); count += c) {
  }
  if (count != nbytes) {
    return NULL;
  }
  cp[nbytes] = 0;
#ifdef DEBUG_PARSE
printf("read_stdin: read %d bytes okay\n", nbytes);
#endif
  return cp;
}

int
write_file(char *filename, char *cp, int nbytes)
{
  int fd = open(filename, O_WRONLY|O_EXCL|O_CREAT, 0755);
  int count;
  if (fd == -1) {
    return -errno;
  }
  count = write(fd, cp, nbytes);
  if (count == -1) {
    return -errno;
  }
  if (count != nbytes) {
    return -1;
  }
  return count;
}

static int
write_stdout(const char *file)
{
  int fd;
#define BUF_SIZE 4096
  char *buf[BUF_SIZE];
  int num, total = 0;

  fd = open(file, O_RDONLY);
  if (fd == -1) {
    return -errno;
  }
  while ((num = read(fd, buf, BUF_SIZE)) > 0) {
    if (num == -1) {
      return -errno;
    }
    int err = write(1, buf, num);
    if (err == -1) {
      return -errno;
    }
    total += num;
  }
  if (num == -1) {
    return -errno;
  }

  return total;
}

#define MAXPARTS 10

void
find_parts(char *cp, int nbytes, char *patstr, int patlen, char **parts)
{
  char **pp, *data;;
  bzero(parts, sizeof(*parts)*MAXPARTS);
  for (data = cp, pp = parts; data < cp + nbytes; data++) {
    if (memcmp(data, patstr, patlen) == 0) {
#ifdef DEBUG_PARSE
      printf("find_parts: offset %d\n", data - cp);
#endif
      *pp++ = data;
      data += patlen;
    }
  }
}


void
process_part(char *pp, char *partend, char *headend)
{
  char *firstheader;
  int headbytes, databytes;
  headend += 4;
  headbytes = headend - pp;
  databytes = partend - headend - 4;
#ifdef DEBUG_PARSE
  printf("header %d [%.*s]\n", headbytes, headbytes, pp);
  printf("data %d bytes [%.*s]\n", databytes, databytes, headend);
#endif
  firstheader = strstr(pp, "\r\n");
  if (firstheader) {
const char *dataheader = "Content-Disposition: form-data; name=\"theuploadfilename\";";
const char *appheader = "Content-Disposition: form-data; name=\"app\"";
    if (strncmp(firstheader + 2, dataheader, strlen(dataheader)) == 0) {
      g_data = headend;
      g_databytes = databytes;
    } else if (strncmp(firstheader + 2, appheader, strlen(appheader)) == 0) {
      if (databytes < MAX_APP_NAME) {
        strncpy(g_app, headend, databytes);
        g_app[databytes] = 0;
      }
    }
  }
}

void
process_parts(char *cp, int nbytes, char **parts)
{
#ifdef DEBUG_PARSE
printf("process_parts nbytes %d\n", nbytes);
#endif
  char *pp;
  while (pp = *parts++) {
    char *partend = *parts;
    char *headend = strstr(pp, "\r\n\r\n");
#ifdef DEBUG_PARSE
    printf("offset %d\n", pp - cp);
#endif
    if (headend != NULL) {
      process_part(pp, partend, headend);
    }
  }
}

int
zip(char *zfile, char *a, char *b)
{
  char buf[80];
  /* -j to "junk" full path */
  sprintf(buf, "/usr/bin/zip -j %s %s %s", zfile, b, a);
  return system(buf); /* -1 on error */
}

const char*
mystrnstr(const char *haystack, const char *needle, size_t len)
{
  int nlen = strlen(needle);
  const char *cp = haystack;
  for (; len >= nlen; len--,cp++) {
    if (strncmp(cp, needle, nlen) == 0) { /* found a match */
      return cp;
    }
  }
  return NULL; /* fewer than nlen chars left, so no match */
}


void deboog(const WhiteList& white_list, const char* app) {
  printf("Content-type: text/plain\n\n");
  if (strcmp(app, "deboog") == 0) {
    printf("Your IP address: %s\n", g_remote_addr);
  } else {
    printf("REMOTE_ADDR %s\n", g_remote_addr);
    printf("REMOTE_HOST %s\n", g_remote_host);
    printf("REMOTE_USER %s\n", g_remote_user);
    printf("allow_access %d\n", white_list.Allow(g_remote_addr));
    white_list.Debug(g_remote_addr);
  }
}

void CleanupAndExit(const char* tmpdir, int exitstatus) {
  unlink(g_zipfilename);
  unlink(g_inputfilename);
  unlink(g_intermediatefilename);
  unlink(g_resultfilename);
  unlink(g_commentfilename);
  if (tmpdir) {
    rmdir(tmpdir);
  }
  exit(exitstatus);
}

int main(int argc, char **argv)
{
  pid_t pid;
  int tofv4[2];
  int fromfv4[2];
  int err = -2;
  int status;
  int exitstatus = 1;

  char *cl;
  const char *boundary = "boundary=";
  char *bpatstr;
  int bpatlen;

  int nbytes;
  char *cp;

  char *ct;

  char *data;
  char *parts[MAXPARTS];

  char *tmpdir = NULL;

  if (chdir("dir") == -1) {
    CleanupAndExit(tmpdir, exitstatus);
  }

#if defined(DEBUG_PARSE) || defined(DEBUG_UPLOAD)
#ifdef DEBUG_DOWNLOAD
  printf("Content-type: application/octet-stream\n\n");
#else
  printf("Content-type: text/plain\n\n");
  printf("DEBUG_PARSE\n");
#endif
#endif

  cl = getenv("CONTENT_LENGTH");
#ifdef DEBUG_PARSE
  if (cl) {
    printf("CONTENT_LENGTH [%s]\n", cl);
  } else {
    printf("No CONTENT_LENGTH?\n");
  }
#endif
  if (cl == NULL) {
    CleanupAndExit(tmpdir, exitstatus);
  }

  ct = getenv("CONTENT_TYPE");
#ifdef DEBUG_PARSE
  if (ct) {
    printf("CONTENT_TYPE [%s]\n", ct);
  } else {
    printf("No CONTENT_TYPE?\n");
  }
#endif
  if (ct == NULL) {
    CleanupAndExit(tmpdir, exitstatus);
  }

  g_remote_addr = getenv("REMOTE_ADDR");
  g_remote_host = getenv("REMOTE_HOST");
  g_remote_user = getenv("REMOTE_USER");

#ifdef ENABLE_WHITE_LIST
{
  WhiteList* white_list = WhiteList::CreateWhiteList(WHITE_LIST);
  if (!white_list) {
    goto cleanup;
  }

  if (strncmp(g_app, "deboog", 6) == 0) {
    deboog(*white_list, g_app);
    goto cleanup;
  }
  
  if (!white_list->Allow(g_remote_addr)) {
    print_access_denied();
    goto cleanup;
  }
}
#else
#ifdef DEBUG_PARSE
printf("white list disabled\n");
#endif
#endif

  nbytes = atoi(cl);
#ifdef DEBUG_PARSE
printf("nbytes %d\n", nbytes);
#endif
  if (nbytes > MAX_UPLOAD_BYTES) {
    print_toobig(nbytes);
    CleanupAndExit(tmpdir, exitstatus);
  }
  cp = read_stdin(nbytes);
  if (cp == NULL) {
    CleanupAndExit(tmpdir, exitstatus);
  }

  bpatstr = strstr(ct, boundary);
  bpatstr += strlen(boundary);
  bpatlen = strlen(bpatstr);

#ifdef DEBUG_PARSE
printf("boundary len: %d str: [%s]\n", bpatlen, bpatstr);
#endif

  find_parts(cp, nbytes, bpatstr, bpatlen, parts);

  process_parts(cp, nbytes, parts);

  if (strncmp(g_app, "getip", 5) == 0) {
    printf("Content-type: text/plain\n\n");
    printf("Your IP address: %s\n", g_remote_addr);
    goto cleanup;
  }

  strcpy(g_dirname, DIR_NAME_TEMPLATE);
  tmpdir = mkdtemp(g_dirname);
  if (tmpdir == NULL) {
    goto cleanup;
  }

  strcpy(g_inputfilename, g_dirname);
  strcat(g_inputfilename, "/in");

  strcpy(g_intermediatefilename, g_dirname);
  strcat(g_intermediatefilename, "/inter");

  strcpy(g_resultfilename, g_dirname);
  strcat(g_resultfilename, "/result.tex");

  strcpy(g_commentfilename, g_dirname);
  strcat(g_commentfilename, "/comment.tex");

  strcpy(g_zipfilename, g_dirname);
  strcat(g_zipfilename, "/a.zip");

#ifdef DEBUG_PARSE
printf("g_dirname %s\n", g_dirname);
printf("g_inputfilename %s\n", g_inputfilename);
printf("g_intermediatefilename %s\n", g_intermediatefilename);
printf("g_resultfilename %s\n", g_resultfilename);
printf("g_commentfilename %s\n", g_commentfilename);
printf("g_app %s\n", g_app);
printf("g_databytes %d\n", g_databytes);
// Since there's just one output the DEBUG_PARSE mode here stops.
// Can't mix .zip (binary) and text output.
exitstatus = 0;
goto cleanup;
#endif

  status = write_file(g_inputfilename, g_data, g_databytes);
  if (status < 0) {
    goto cleanup;
  }

#ifdef DEBUG_DOWNLOAD
fflush(stdout);
write_stdout(g_inputfilename);
#endif

#ifdef DEBUG_UPLOAD
exitstatus = 0;
goto cleanup;
#endif

  if (pipe(tofv4) == -1) {
    goto cleanup;
  }
  if (pipe(fromfv4) == -1) {
    goto cleanup;
  }

  pid = fork();
  if (pid == -1) {
    goto cleanup;
  }
  if (pid == 0) { /* child */
    char *argv[7];
    argv[0] = g_app;
    argv[1] = g_inputfilename;
    argv[2] = g_intermediatefilename;
    argv[3] = g_resultfilename;
    argv[4] = g_commentfilename;
    argv[6] = NULL;
    /* parent writes to child stdin... */
    if (dup2(tofv4[0], 0) == -1) {
      goto cleanup;
    };
    /* ... and reads from child stdout*/
    if (dup2(fromfv4[1], 1) == -1) {
      goto cleanup;
    };
    /* Pass input, temp, and output files */
    if (execve(g_app, argv, 0) == -1) {
      goto cleanup;
    }
    /* NOTREACHED */
  } else { /* parent */
    pid_t p;
    p = wait(&status);
    if (WIFEXITED(status) && (WEXITSTATUS(status) == 0)) {
      err = zip(g_zipfilename, g_resultfilename, g_commentfilename);
      if (err == -1) {
        goto cleanup;
      }

      //printf("Content-type: application/octet-stream\n\n");
      printf("Content-type: application/zip\n");
      printf("Content-Disposition: Attachment; filename=output.zip\n\n");
      fflush(stdout);
      err = write_stdout(g_zipfilename);
      if (err > 0) {
        exitstatus = 0; /* all is well */
        goto cleanup;
      }
    }
  }

  /* fall through to here on error */
  printf("Content-type: text/plain\n\n");
  printf("wifexited %d, wexitstatus %d, err %d\n",
    WIFEXITED(status), WEXITSTATUS(status), err);
cleanup:
  unlink(g_zipfilename);
  unlink(g_inputfilename);
  unlink(g_intermediatefilename);
  unlink(g_resultfilename);
  unlink(g_commentfilename);
  if (tmpdir) {
    rmdir(tmpdir);
  }
  exit(exitstatus);
}
